一、简介
CVE-2018-4233是Pwn2Own 2018上Samuel Groß团队用来攻破Safari浏览器的漏洞。这是一个JIT编译器中side effect导致IR建模失败而产生的一个类型混淆漏洞,通过这样的类型混淆漏洞可以得到addrof、fakeobj两个原语,从而直接获得沙箱内远程代码执行的效果。
CVE-2018-4233是JIT中的类型混淆漏洞中的一个经典,非常有代表性。
二、漏洞概要
该漏洞的fix位于https://github.com/WebKit/webkit/commit/b602e9d167b2c53ed96a42ed3ee611d237f5461a,本文基于其parent commit
7996e60。该版本更新时间是2018年3月末,PoC:
1 | let someObject = {}; |
三、基本概念
JIT:即时编译,边运行边产生机器代码,常见于浏览器等脚本执行环境,使用机器码替换高级语言的代码来运行,代替解释器以优化运行速度。
DFG:dfg在编译原理中用来代表数据流图,在WebKit中是第一层优化编译器:
- LLInt -> baseline JIT -> DFG JIT -> FTL JIT,函数、循环经过多次执行,从解释器到各优化编译器会进行tier up,是从左到右的过程。在这一过程中,JIT编译器会根据之前运行获得(profile)的类型信息对函数体中参数、变量的类型进行假定,生成代码执行(speculative execute)。
- JIT编译器在执行遇到问题的时候会进行tier down,是从右到左的过程,在WebKit中被称为OSRExit,其实现包括Jump replacement等机制。
side-effect:也被译为“副作用”,更新变量和数据结构的赋值语句。
IR建模:JIT优化编译器需要对中间表示(IR)的字节码进行建模,精确描述side effect等。在WebKit中有两处代码用于建模:DFGAbstractInterpreter、DFGClobberize,前者被简称为AI。
JSValue:JSValue是WebKit JavaScriptCore中用来表示和存储JS执行上下文中对象、整数、浮点数的而定义的一个类。其中重点关注:
- ArrayWithDouble,JS中的双精度浮点数数组,如果一个Array中只包含浮点数,它就是一个ArrayWithDouble类型的数组,其中的双精度浮点数按照IEEE754的原始浮点数存储。例如3.54484805889626e-310在内存中存储为0x0000414141414141。
- ArrayWithContiguous,JS中的普通数组,如果一个Array中包含浮点数、对象等,它就是一个ArrayWithContiguous类型的数组,其中元素按照JSValue方式存储,其中对象指针用48位表示,前16位为0,而浮点数加上了0x0001000000000000。例如3.54484805889626e-310在内存中存储为0x0001414141414141。
ArrayWithDouble、ArrayWithContiguous中浮点数和指针的互相赋值是构造类型混淆漏洞的方法。
在JIT类型混淆漏洞中,IR建模错误往往是漏洞产生的根本原因。一般过程是:
- 通过IR建模错误,
- 利用side effect产生回调改变变量类型,
- 借助类型特化过的JIT代码,
- 赋值ArrayWithContiguous中的元素到ArrayWithDouble中,
- 得到类型混淆。
四、漏洞分析
本部分以fakeobj这个原语为例解析这个漏洞,addof原语的构造与其极其相似,不需要调试稍作修改即可。
设置调试目标为jsc,参数添加:
1 | --useConcurrentJIT=false |
开始运行,从输出中可见,assign、get产生了DFG JIT代码:
1 | Generated DFG JIT code for assign#EmOb1Y:[0x11cd78720->0x11cd78260->0x11cd98f20, DFGFunctionConstruct, 24], instruction count = 24: |
4.1 第一次OSRExit
随后产生了Firing watchpoint,并丢弃get的dfg代码:
1 | Firing watchpoint 0x11c86f398 on get#CsfvpT:[0x11cd78980->0x11cd784c0->0x11cd98fd0, DFGFunctionCall, 60] |
栈回溯可见,它的产生是由于JIT::operationPutToScope,显然这个栈回溯来自代理中get handler中的赋值arr[0] = {};
。
之后是相应的JumpReplacement:
1 | Firing jump replacement watchpoint from 0x3729250003ee, to 0x37252900067f. |
Jump replacement位于DFG JIT code for get,大概位置是:
1 | SetArgument |
4.2 第二次OSRExit
1 | Firing watchpoint 0x11c86f438 on get#CsfvpT:[0x11cd78980->0x11cd784c0->0x11cd98fd0, DFGFunctionCall, 60] |
此次的栈回溯为:
其中的重点是:
JSC::JSObject::convertDooubleToContiguousWhilePerformingSetIndex
JIT::operationPutByVal
0x3728e500268c baseline JIT code for get# [put_by_val]
JSC::ProxyObject::getOwnPropertySlot
JSC::JSObject::get 回调发生处
dfg::operationCreateThis(0x100d53cd0)
0x372925000c05 DFG JIT code for assign#
JIT::operationLinkCall
0x3728e50038d3 Baseline JIT code for primitiveFakeObj# [construct]
随后有:
1 | Firing watchpoint 0x11c86f208 on assign#EmOb1Y:[0x11cd78720->0x11cd78260->0x11cd98f20, DFGFunctionConstruct, 24] |
DFG jump 位于DFG JIT for assign:
1 | ... |
从打印数据里看CreateThis的执行调用operationCreateThis,然后从JSC::JSObject::get中回调触发assign赋值(ConvertDoubleToContiguous),然后触发Jump replacement,但显然Jump replacement的跳出点位于CreateThis之前,而CreateThis之后不再有跳出点。意即:虽然作出了OSRExit的动作,但并没有成功OSRExit,这里就是bug的直观表现了。
五、补丁分析
针对这个漏洞,补丁围绕对CreateThis字节码进行建模修改了两处内容clobberize、AbstractInterpreter。下面分别考察这两部分的作用。在这个过程中,可以多次修改代码、编译运行程序、dump内存、放入反汇编器,便于阅读JIT代码提高效率。
用于dump内存的lldb命令为:
1 | me read -o /Users/dwfault/Downloads/dump.bin -b 0x31fe33000500 0x31fe33001000 --force |
5.1 考察clobberize
右图的字节码对应write(Heap)补丁之后的代码。可以看到IR中的CreateThis后面增加了一个InvalidationPoint,其他都没有改变。但由于此处相应会产生一个Jump replacement(参考DFGInvalidationPointInjectionPhase),导致CreateThis回调结束之后会被Jump replacement引到OSRExit,使漏洞不再触发。
5.2 考察AbstractInterperter
按照同样的方法,现在为CreateThis只加上clobberWorld。
可以看到在GetButterfly之前增加了一个CheckStrucure,其x86机器码是:
1 | cmp dword [rax], 0x5b |
细致调试可以知道,arr变量的structureID之前是0x5b,现在成了0x5c,这也使得执行流程走向了OSRExit,使漏洞不再发生。
5.3 深究clobberize与AbstractInterpreter
可以看到在5.1、5.2两节中,clobberize、AbstractInterpretrer两处对IR建模的描述直接导致了JIT编译器产生了不同的代码。其中5.1节clobber与InvalidationPoint的关系非常明确,在DFGInvalidationPointInjectPhase中可以很明确地知道:
(1) DFGClobberize把Abstract Heap以树型结构分为几类:
- World
- Stack
- Heap
- Other(Top)
- Other(none Top)
- Watchpoint_fire
- SideState
- Other(Top)
(2) writesOverlap函数检查目标opcode结点的write属性与WatchPoint_fire在树型结构中的父子关系,如果WatchPoint_fire在目标opcode结点的子树上,则插入InvalidationPoint。
而对于AbstractInterpreter,clobberWorld的作用不明显,需要加以更多调试。 回到漏洞版本,增加运行选项运行:
1 | --dumpDFGGraphAtEachPhase=true |
得到的输出节选如下:
1 | Beginning DFG phase structure check hoisting. |
CheckStructure这个IR有一个专门的提前阶段,叫DFGCheckStructureHoisting。这个过程把42 GetLocal、43 CheckStructure提前。对比clobberWorld版本这个阶段也是发生的。
随后,有:
1 | Beginning DFG phase constant folding. |
可以发现,GetButterfly之前的CheckStructure在常量折叠过程被优化掉了;对比clobberWorld版本,GetButterfly之前的CheckStructure仍保留。因此clobberWorld最终影响了constant folding phase。
跟踪constant folding phase,第187行的node->remove(m_graph)直接导致CheckStructure从IR的控制流图中被删除:
进入if结构需要满足185行的if表达式为真,那么value.m_structure.isSubsetOf(set)就很重要了。
重新调试,为了找到对clobberWorld的调用,在DFG::AbstractInterpreterInlines.h的executeEffects函数case CreateThis选项下断点。发现停在了CFA phase(Control Flow Analysis)阶段,而这几个phase的顺序是:
invalidationpoint injection -> structure check hoisting -> strength reduction -> cps rethreading -> cfa -> constant folding
executeEffects是在CFA阶段内发生的,那么CFA之前的阶段不需要关注。
跟进clobberWorld,在多层的调用中,有对m_set.setReservedFlag的赋值:
1 | static const uintptr_t reservedFlag = 2; |
之后,在constant folding phase:
跟进value.m_structure.isSubsetof(set):
由此解决了clobberWorld在AbstractInterpreter的前后呼应问题。
六、总结
CVE-2018-4233可以总结为CreateThis side effect导致的类型混淆漏洞,补丁修改了两处位置,AbstractInterpreter、Clobberize,这两处的任意一个均可以使PoC失效。相比2017年Moblie Pwn2Own中Vulcan团队使用的GetPropertyEnumerator/HasGenericProperty side effect漏洞的补丁,这个补丁更加全面地展示了这个漏洞的模式和面貌。挖掘出这种类型的漏洞,只需要两步:
- 寻找具有回调特性的DFG IR
- 寻找DFG IR的模型问题
然后就可以尝试构造漏洞了。